/**
 * 
 */
package vn.smartware.co.java.net.http;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import vn.smartware.co.java.net.commons.BytesInputStream;
import vn.smartware.co.java.net.commons.BytesOutputStream;
import vn.smartware.co.java.net.commons.Const;
import vn.smartware.co.java.net.commons.Utils;
import vn.smartware.co.java.net.exception.JHTTPClientException;
import vn.smartware.co.java.net.exception.JHTTPServerException;
import vn.smartware.co.java.net.exception.TimeoutException;

/**
 * JHTTPClient
 * 
 * @author thuctt
 */
public class JHTTPClient {
	
	private static Log log = LogFactory.getLog(JHTTPClient.class);
	
	private static HttpURLConnection con;
	private static final String APP_SESSION = "application";
	private static final String SESSION_SESSION = "session";
	private static final String REQUEST_COOKIE = "Cookie";
	private static final String RESPONCE_COOKIE = "Set-Cookie";

	private String urlAdress;
	private int timeout;
	private int maxRetry;
	private int intervalRetry;
	private String context;
	private String encoding;
	private boolean sslFlag;
	private String sendMethod;
	
	private String scope;
	
	private int numRetry = 0;
	
	/**
	 * Constructor
	 */
	public JHTTPClient() {
	}
	
	/**
	 * Constructor
	 * @param addr 
	 * @throws JHTTPClientException 
	 */
	public JHTTPClient (String addr) throws JHTTPClientException {
		
		this.sslFlag = checkURL(addr);
		this.urlAdress = addr;
		this.timeout = Const.DEFAULT_TIMEOUT;
		this.maxRetry = Const.DEFAULT_MAX_RETRY;
		this.intervalRetry = Const.DEFAULT_INTERVAL_RETRY;
		this.encoding = Const.DEFAULT_ENCODING;
		this.context = Const.CONTENT_TYPE_OCTEC_STREAM;
		this.sendMethod = Const.DEFAULT_METHOD;
	}
	
	/**
	 * Constructor
	 * @param addr 
	 * @param contentType 
	 * @param timeout 
	 * @param encoding 
	 * @throws JHTTPClientException 
	 */
	public JHTTPClient(String addr, String contentType, int timeout,
			String encoding) throws JHTTPClientException {
		
		this.sslFlag = checkURL(addr);
		this.urlAdress = addr;
		this.timeout = timeout;
		this.context = contentType;
		this.encoding = encoding;
		this.maxRetry = Const.DEFAULT_MAX_RETRY;
		this.intervalRetry = Const.DEFAULT_INTERVAL_RETRY;
	}
	
	/**
	 * check URL
	 * @param urlAddr 
	 * @return boolean
	 * @throws JHTTPClientException
	 */
	private boolean checkURL(String urlAddr) throws JHTTPClientException {
		
		if (Utils.isNullEmpty(urlAddr)) {
			log.error("URL can not be null.");
			throw new JHTTPClientException();
		}
		//
		if (urlAddr.toLowerCase().startsWith(Const.HTTP_PREFIX)) {
			return false;
		} else if (urlAddr.toLowerCase().startsWith(Const.HTTPS_PREFIX)) {
			return true;
		} else {
			throw new JHTTPClientException();
		}
	}
	
	/**
	 * connect
	 * @throws JHTTPClientException
	 */
	public void connect() throws JHTTPClientException {

		if (Utils.isNullEmpty(this.context)){
			throw new JHTTPClientException();
		}
		
		try {
			
			if (!sslFlag) {
				log.info("HTTP Connection start... URI=" + this.urlAdress);
			}else{
				log.info("HTTPS Connection start... URI=" + this.urlAdress);
			}
			
			URL url = new URL(this.urlAdress);
			con = (HttpURLConnection) url.openConnection();
			
			//set propeties
			con.setRequestProperty("Content-Type", context);
			con.setRequestProperty("User-agent", Const.OS);
			
			con.setDoOutput(true);
			con.setDoInput(true);
			con.setUseCaches (false);
			
			if (this.scope == null || this.scope.equals(APP_SESSION)) {
				handlingSendCookie();
			} else if (this.scope.equals(SESSION_SESSION)) {
				longHandlingSendCookie();
			}
			
			if (sslFlag){
				compulsorilyTrust();
			}
			
			con.setRequestMethod("POST");
			
			System.setProperty ( "sun.net.client.defaultConnectTimeout", Integer.toString(this.timeout)); 
			System.setProperty ( "sun.net.client.defaultReadTimeout", Integer.toString(this.timeout));
			
			con.connect();
			
			if (!sslFlag) {
				log.info("HTTP Connected.");
			}else{
				log.info("HTTPS Connected.");
			}

			numRetry = 0;
			
		} catch (Exception e) {
			numRetry++;
			if (numRetry < maxRetry){
				log.warn("JHTTPClient connect failed. Retry = " + numRetry);
				
				try{
					Thread.sleep(intervalRetry);
				}catch (InterruptedException ee) {
					throw new JHTTPClientException(ee.getMessage());
				}
				
				connect();
			}
			log.error("JHTTPClient connect Exception. " + e.getMessage());
			throw new JHTTPClientException("JHTTPClient connect Exception. ", e);
		} 
	}
	
	/**
	 * send a request
	 * 
	 * @param content
	 * @throws JHTTPClientException
	 */
	public void sendRequest(String content) throws JHTTPClientException {

		OutputStreamWriter osw = null;
		try {
			osw = new OutputStreamWriter( con.getOutputStream(), encoding );
			osw.write(content);
			osw.flush();
			
			osw.close();
			osw = null;
			
		} catch (IOException e) {
			numRetry++;
			if (numRetry < maxRetry) {
				log.warn("JHTTPClient sendRequest failed. Retry = " + numRetry);
				try{
					Thread.sleep(intervalRetry);
				}catch (InterruptedException ee) {
					throw new JHTTPClientException("JHTTPClient sendRequest InterruptedException.", ee);
				}
				sendRequest(content);
			}
			
			log.info("JHTTPClient sendRequest IOException." + e.getMessage());
			throw new JHTTPClientException("JHTTPClient sendRequest IOException.", e);
				
		} finally {
			numRetry = 0;
		}
	}
	
	/**
	 * send a request
	 * 
	 * @param obj
	 * @throws JHTTPClientException 
	 */
	public void sendRequest(Object obj) throws JHTTPClientException {

		ObjectOutputStream osw = null;
		try {
			osw = new ObjectOutputStream( con.getOutputStream() );
			osw.writeObject(obj);
			osw.flush();
			
			osw.close();
			osw = null;
			
		} catch (IOException e) {
			numRetry++;
			if (numRetry < maxRetry) {
				log.warn("JHTTPClient sendRequest failed. Retry = " + numRetry);
				
				try {
					Thread.sleep(intervalRetry);
				} catch (InterruptedException ee) {
					throw new JHTTPClientException("JHTTPClient sendRequest InterruptedException.", ee);
				}
				
				sendRequest(obj);
			}
			log.info("JHTTPClient sendRequest IOException." + e.getMessage());
			throw new JHTTPClientException("JHTTPClient sendRequest IOException.", e);
			
		} finally {
			numRetry = 0;
		}
	}
		
	/**
	 * receive response from server
	 * @return String
	 * @throws TimeoutException 
	 * @throws JHTTPServerException 
	 */
	public String rcvResponceString() throws TimeoutException, JHTTPServerException {

		String out = null;
		InputStreamReader reader = null;
		
		try {			
			//StatusCode Check
			if (con.getResponseCode() == HttpURLConnection.HTTP_OK) {

				StringBuffer buffer = new StringBuffer();
				reader = new InputStreamReader(con.getInputStream(), this.encoding);
				int c;
				while ((c = reader.read()) != -1) {
		            buffer.append((char) c);
		        }
				out = buffer.toString();
				log.info("Responce Content=" + out);
				
				reader.close();
				reader = null;
				
			}else if (con.getResponseCode() == HttpURLConnection.HTTP_CLIENT_TIMEOUT) {
				log.error("HTTP Responce error: Request timeout");
				throw new TimeoutException(Integer.toString(con.getResponseCode()));
			}else{
				log.info("HTTP Responce error. Error code: " + con.getResponseCode());
				throw new JHTTPServerException(Integer.toString(con.getResponseCode()));
			}
			
		} catch (Exception e) {
			numRetry++;
			if (numRetry < maxRetry){
				log.warn("JHTTPClient rcvResponceString failed. Retry = " + numRetry);
				try{
					Thread.sleep(intervalRetry);
				}catch (InterruptedException ee) {
					throw new JHTTPServerException("JHTTPClient rcvResponceString InterruptedException.", ee);
				}
				rcvResponceObject();
			}
			log.error("JHTTPClient rcvResponceString Exception." + e);
			throw new JHTTPServerException("JHTTPClient rcvResponceString Exception.", e);
			
		} finally {
			numRetry = 0;
		}
		return out;
	}
	
	/**
	 * receive response from server
	 * @return Object
	 * @throws TimeoutException 
	 * @throws JHTTPServerException 
	 */
	public Object rcvResponceObject() throws TimeoutException,
			JHTTPServerException {

		InputStream in = null;
		BufferedInputStream bis = null;
		ObjectInputStream ois = null;
		
		try {
			if (scope == null || scope.equals(APP_SESSION)) {
				handlingReceiveCookie();
			} else if (scope.equals(SESSION_SESSION)) {
				longHandlingReceiveCookie();
			}
			
			//StatusCode Check
			if (con.getResponseCode() == HttpURLConnection.HTTP_OK
					|| con.getResponseCode() == HttpURLConnection.HTTP_ACCEPTED) {

				in = con.getInputStream();
				
				int contentLength = con.getContentLength();

				log.info("Responce Content-Length=" + contentLength);
				
				bis = new BufferedInputStream(in);
				
				byte[] buf = new byte[contentLength];
				ByteBuffer byteBuf = ByteBuffer.allocate(buf.length);

				int len;
				while((len = bis.read(buf)) != -1) {
					if (len != 0) {
						byteBuf.put(buf, 0, len);
					} else {
						log.error("Responce Content-Length =" + contentLength);
						throw new JHTTPServerException("Responce Content-Length =" + contentLength);
					}
				}

				ois = new ObjectInputStream(new BytesInputStream(byteBuf.array()));

				Object bean = ois.readObject();
				
				ois.close();
				ois = null;
				bis.close();
				bis = null;
				in.close();
				in = null;
				
				return bean;
				
			}else if (con.getResponseCode() == HttpURLConnection.HTTP_CLIENT_TIMEOUT) {
				log.error("JHTTPClient rcvResponceObject error: Request timeout");
				throw new TimeoutException(Integer.toString(con.getResponseCode()));
			}else{
				log.info("JHTTPClient rcvResponceObject error. Error code: " + con.getResponseCode());
				throw new JHTTPServerException(Integer.toString(con.getResponseCode()));
			}
			
		} catch (Exception e) {
			numRetry++;
			if (numRetry < maxRetry){
				log.warn("JHTTPClient rcvResponceObject failed. Retry = " + numRetry);
				try{
					Thread.sleep(intervalRetry);
				}catch (InterruptedException ee) {
					throw new JHTTPServerException("JHTTPClient rcvResponceObject InterruptedException.", ee);
				}
				rcvResponceObject();
			}
			log.error("JHTTPClient rcvResponceObject Exception." + e);
			throw new JHTTPServerException("JHTTPClient rcvResponceObject Exception.", e);
			
		} finally {
			numRetry = 0;
		}
	}
	
	/**
	 * receive request from client
	 * @param request
	 * @return Object
	 * @throws IOException 
	 * @throws ClassNotFoundException 
	 */
	public static Object rcvRequest(HttpServletRequest request)
			throws IOException, ClassNotFoundException {
		
		Object obj = null;
		ObjectInputStream ois = new ObjectInputStream(request.getInputStream());

		obj = ois.readObject();
		
		ois.close();
		ois = null;
		
		return obj;
	}
	
	/**
	 * send response to client
	 * @param response
	 * @param sentObj 
	 * @throws IOException 
	 */
	public static void sendResponse(HttpServletResponse response, Object sentObj)
			throws IOException {
		
		BytesOutputStream byteStream = new BytesOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(byteStream);
		
		oos.writeObject(sentObj);
		oos.flush();
		
		byte[] buf = byteStream.toByteArray();
		
		byteStream.writeTo(response.getOutputStream());
		byteStream.write(buf, 0, buf.length);
		byteStream.flush();
		
		oos.close();
		byteStream.close();
		oos = null;
		byteStream = null;
	}
	
	/**
	 * handlingReceiveCookie
	 */
	private void handlingReceiveCookie() {
		
		String cookieValue = con.getHeaderField(RESPONCE_COOKIE);

		if (cookieValue != null) {
			
			String sessionID = cookieValue.substring(0, cookieValue.indexOf(";"));

			log.info("Long receive SessionID: " + sessionID);

			Session.getInstance().setSessionID(sessionID);
		}
	}
	
	/**
	 * longHandlingReceiveCookie: 
	 */
	private void longHandlingReceiveCookie() {

		String cookieValue = con.getHeaderField(RESPONCE_COOKIE);

		if (cookieValue != null) {
			
			String sessionID = cookieValue.substring(0, cookieValue.indexOf(";"));

			log.info("Long receive SessionID: " + sessionID);

			LongSession.getInstance().setSessionID(sessionID);
		}
	}
	
	/**
	 * check SSL
	 * @throws JHTTPServerException
	 */
	private void compulsorilyTrust() throws JHTTPServerException {
		
		SSLSocketFactory sslsf = null;		
		javax.net.ssl.KeyManager[] km = null;

		try {
			
			//
			TrustManager[] tm = { new RelaxedX509TrustManager() };
			
			//
			SSLContext sslContext = SSLContext.getInstance("SSL");
			
			//
			sslContext.init(km, tm, new SecureRandom());
			
			//
			sslsf = sslContext.getSocketFactory();
			
			//
			((HttpsURLConnection)con).setSSLSocketFactory(sslsf);
			
			HostnameVerifier hv = new HostnameVerifier() {
				public boolean verify(String name, SSLSession session) {
					return true;
				}
			};
			
			((HttpsURLConnection)con).setHostnameVerifier(hv);

		} catch (KeyManagementException e) {
			log.error("Connection Trust failed:" + this.urlAdress);
			throw new JHTTPServerException("Connection Trust failed:" + this.urlAdress, e);
		} catch (NoSuchAlgorithmException e) {
			log.error("Connection Trust failed:" + this.urlAdress);
			throw new JHTTPServerException("Connection Trust failed:" + this.urlAdress, e);
		}
	}
	
	/**
	 * disconnect from server
	 */
	public void disconnect(){
		
		if (null != con){
			con.disconnect();
			con = null;
			log.info("Disconnected.");
		}
	}

	/**
	 * @return the context
	 */
	public String getContext() {
		return context;
	}

	/**
	 * @param context the context to set
	 */
	public void setContext(String context) {
		this.context = context;
	}

	/**
	 * @return the encoding
	 */
	public String getEncoding() {
		return encoding;
	}

	/**
	 * @param encoding the encoding to set
	 */
	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	/**
	 * @return the intervalRetry
	 */
	public int getIntervalRetry() {
		return intervalRetry;
	}

	/**
	 * @param intervalRetry the intervalRetry to set
	 */
	public void setIntervalRetry(int intervalRetry) {
		this.intervalRetry = intervalRetry;
	}

	/**
	 * @return the maxRetry
	 */
	public int getMaxRetry() {
		return maxRetry;
	}

	/**
	 * @param maxRetry the maxRetry to set
	 */
	public void setMaxRetry(int maxRetry) {
		this.maxRetry = maxRetry;
	}

	/**
	 * @return the numRetry
	 */
	public int getNumRetry() {
		return numRetry;
	}

	/**
	 * @param numRetry the numRetry to set
	 */
	public void setNumRetry(int numRetry) {
		this.numRetry = numRetry;
	}

	/**
	 * @return the timeout
	 */
	public int getTimeout() {
		return timeout;
	}

	/**
	 * @param timeout the timeout to set
	 */
	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	/**
	 * @return the urlAdress
	 */
	public String getUrlAdress() {
		return urlAdress;
	}

	/**
	 * @param urlAdress the urlAdress to set
	 */
	public void setUrlAdress(String urlAdress) {
		this.urlAdress = urlAdress;
	}

	/**
	 * @return the sendMethod
	 */
	public String getSendMethod() {
		return sendMethod;
	}

	/**
	 * @param sendMethod the sendMethod to set
	 */
	public void setSendMethod(String sendMethod) {
		this.sendMethod = sendMethod;
	}
	
	/**
	 * set SESSION
	 * @param scope
	 */
	public void setScope(String scope){
		
		this.scope = scope;
	}
	
	/**
	 * handlingSendCookie: 
	 */
	private void handlingSendCookie() {
		
		String sessionID = Session.getInstance().getSessionID();
		
		log.info("Send SessionID: " + sessionID);
		
		if (sessionID != null) {
			con.setRequestProperty(REQUEST_COOKIE, sessionID);
		}
	}
	
	/**
	 * longHandlingSendCookie: 
	 */
	private void longHandlingSendCookie() {
		
		String sessionID = LongSession.getInstance().getSessionID();
		
		log.info("Send SessionID: " + sessionID);
		
		if (sessionID != null) {
			con.setRequestProperty(REQUEST_COOKIE, sessionID);
		}
	}
}
